package time.compiler.query.solvers;
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.MacroTools;
import time.Errors;
import time.compiler.types.TypeBuilder;

/**
 * This class will be used by the macro to autoBuild the Solvers, using the @:timeApi metadata 
 * as a reference to generate the solver wrapped fields type
 * @author waneck
 */

class Build 
{

	@:macro public static function solver():Expr
	{
		var cl = Context.getLocalClass().get();
		
		//TODO all this code is severely hard-coded, specially the type parameters part
		//The issue is that I don't want to expand the complexity of type parameters to other places other
		//than function solvers. So by now it's going to be like this. As a long-term todo, we need proper
		//support of type parameters if we are going to do this the right way.
		var matchedTypeBuilder = null;
		var api = null;
		for( m in cl.meta.get() )
			if( m.name == ":timeApi" ) {
				if( m.params.length != 2 )
					Context.error("@:timeApi metadata should have two parameters: the matched types, and the actual type definition", m.pos);
				matchedTypeBuilder = m.params[0];
				api = m.params[1];
				break;
			}
		if (api == null)
			Context.error("@:timeApi metadata not found", cl.pos);
			
		var matched = [];
		var typeParam = null;
		function match(e:Expr)
		{
			return switch(e.expr)
			{
				case ECall(type, param):
					switch(MacroTools.getString(type, true))
					{
						case "DBToMany":
							if (param.length != 1)
								throw "invalid number of parameters for DBToMany";
							
							switch(param[0].expr)
							{
								case ECall(_, _): "DBToMany<" + match(param[0]) + ">";
								default:
									var param = MacroTools.getString(param[0], true);
									if (param.substr(0, 1) == "$") //type parameter
									{
										var tp = param.substr(1);
										if (null != typeParam && tp != typeParam)
											throw "Only one type parameter can be set. Please use the same type parameter name on all enabled types";
										typeParam = tp;
										
										"DBToMany<*>";
									} else {
										throw "Invalid DBToMany syntax. If you want to define it as a type parameter, add $ to it";
									}
							}
						case "DBBasic":
							if (param.length != 1)
								throw "invalid number of parameters for DBToMany";
							"DBBasic(" + MacroTools.getString(param[0], true);
						case "DBInst":
							if (param.length != 1)
								throw "invalid number of parameters for DBInst";
							"DBInst(" + MacroTools.getString(param[0], true);
						case "DBEnum":
							if (param.length != 1)
								throw "invalid number of parameters for DBEnum";
							"DBEnum(" + MacroTools.getString(param[0], true);
						
						default:
							throw "Only DBType types can be matched";
					}
				default: throw "Invalid matched type expression";
			}
		}
		
		switch(matchedTypeBuilder.expr)
		{
			case EArrayDecl(vals):
				for (v in vals)
					matched.push(match(v));
			default: matched.push(match(matchedTypeBuilder));
		}
		
		var inits = [];
		switch(api.expr)
		{
			case EBlock(fields):
				for (field in fields)
				{
					switch(field.expr)
					{
						case EFunction(name, f):
						inits.push( { name:name, f:decode(f, typeParam) } );
						default: throw Assert;
					}
				}
			default: trace(api.expr); throw Assert;
		}
		
		var mk = MacroTools.mk;
		var pos = Context.currentPos();
		
		var bl_a = [
			mk(EBinop(OpAssign, mk(EConst(CIdent("matchedTypeBuilder")), pos), Context.makeExpr(matched, pos)), pos)
		];
		
		
		var bl = mk(EBlock(bl_a), pos);
		for (init in inits)
		{
			bl_a.push(mk(ECall( mk( EField(mk(EConst(CIdent("fields")),pos), "set"), pos ), [Context.makeExpr(init.name,pos), Context.makeExpr(init.f, pos) ] ), pos ));
		}
		return bl;
	}
	
	static function decode(f:Function, typeParam:Null<String>)
	{
		var ret:Array<Dynamic> = [];
		var params = new Hash();
		
		if (typeParam != null)
			params.set(typeParam, 1);
		
		var i = 2; //why two? because
		for (p in f.params)
			params.set(p.name, i++);
		
		var decode_arg = null;
		decode_arg = function (ret:Array<Dynamic>, name:Null<String>, c:Null<ComplexType>, ?toMany:Bool = false)
		{
			if (null == c && name == "repeat")
			{
				ret.push("repeat");
			} else if (null != c) {
				switch(c)
				{
					case TPath(p):
						if (p.pack.length == 0 && params.exists(p.name))
						{
							var i = params.get(p.name);
							if (toMany)
								i |= 0x100;
							ret.push(i);
						} else {
							var path = (p.pack.length > 0) ? p.pack.join(".") + "." + p.name : p.name;
							switch(path)
							{
								case "DBToMany":
									//special case for parametered DBToMany type
									if (p.params.length != 1)
										throw "Special DBToMany construct requires a parameter";
									
									if (toMany)
										throw "Invalid DBToMany<DBToMany<>> construct";
									
									switch(p.params[0])
									{
										case TPType(c):
											decode_arg(ret, name, c, true);
										default:
											throw "Invalid TPExpr";
									}
									
								default:
									if (null != path && path == typeParam)
									{
										ret.push(1);
									} else {
										var c = TypeBuilder.getData(path); //if the type doesn't exist an error will be displayed
										ret.push(c);
									}
							}
						}
					case TFunction(args, retv):
						var otherRet = [];
						for (arg in args)
							decode_arg(otherRet, null, arg, toMany);
						decode_arg(otherRet, null, retv, toMany);
						
						ret.push(otherRet);
					default: throw "Invalid type "  + c;
				}
			}
		}
		
		for (arg in f.args)
			decode_arg(ret, arg.name, arg.type, false);
		
		decode_arg(ret, null, f.ret, false);
		
		return ret;
	}
}